Skip to content

fix(hooks): make the dirty list mean "edited since last publish" (#4 + #5) - #11

Merged
A-PachecoT merged 2 commits into
mainfrom
sprint/hook-truth
Aug 17, 2026
Merged

fix(hooks): make the dirty list mean "edited since last publish" (#4 + #5)#11
A-PachecoT merged 2 commits into
mainfrom
sprint/hook-truth

Conversation

@A-PachecoT

@A-PachecoT A-PachecoT commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Closes #4. Closes #5.

#5 carried two distinct problems. This PR fixes problem (1), the dirty-list. Problem (2) — basalt status reporting modified right after a successful publish for docs with local images — is a CLI-side comparator bug and is carried forward as #12 so that closing #5 does not silently close it too.

The proposition the hook got wrong

stop.sh asserted "this doc is unpublished" from a measurement of "this doc was edited". Those are different propositions, and two flows sat entirely inside the gap:

So the agent had no way to satisfy the hook except by not editing. A nudge that cannot be satisfied is not a nudge — it trains the reader to mute the channel, and then the sensor is worth less than nothing.

The list now means: vault docs edited since their last publish.

Two mechanisms, both local — no network added to the hot path

A — post-publish.sh (new), PostToolUse on Bash. Edit-tracking's twin: it appends a tombstone for a doc a command really published. The list became an append-only journal (post-edit.sh appends paths, post-publish.sh appends -<path>, stop.sh replays), so ordering is correct by construction and two O_APPEND writers cannot lose each other's updates.

B — a git-state filter in stop.sh. Nine conditions, all local. Its claim is deliberately narrow: positive local evidence that a push of this file, on this branch, starts a Basalt publish workflow. It does not claim to predict what the Action publishes — the real publish set is decided by a git ls-files | xargs pipeline and filename conventions in shell, which no hook can model. That limit is written into the code, not left implied.

Why not basalt status

It is the obvious fix and it is rejected on two independent grounds. It is network on every Stop with any dirty file, which #4's guardrail closes textually. And #5's own body documents it lying: publish rewrites image refs without touching the local file, so status returns modified immediately after a successful publish for any doc with local images — nearly every doc in the vault where the bug was filed. The fix would have failed exactly where it was reported, and failed silently.

Measured, not assumed

Three harness facts this rests on were captured, because none were previously verified — the existing capability notes cover Edit|Write; Bash had never been captured.

  • PostToolUse(Bash) fires only on exit 0. Measured with a control counting executions separately from hook fires: 4 executions, 2 fires. A failed publish never reaches the hook, which is how success is known without an exit-code field (the payload has none) and without a network call.
  • The payload's cwd does not follow a compound cd. cd /tmp && pwd prints /tmp; the payload still reports the session dir. Since El Stop hook pide publicar docs ya publicados (dirty-list sin publish-state) + status miente en docs con imágenes #5's own repro is cd <vault> && basalt publish …, resolving operands against cwd alone would match nothing — and a hook that matches nothing is indistinguishable from a dead one.
  • ugrep and GNU grep disagree on [^\n] inside a bracket. Local dev and CI would diverge, so there is no regex path parsing here — case globs and string operators only.

Everything ambiguous fails toward the nag

--dry-run · publish f || true and non-final ; segments (exit 0 is a property of the command, not a segment) · quoted or multi-line commands · unresolvable operands · a doc whose extension the workflow's paths: list does not match · a workflow that is untracked, dirty, unpushed, has no push: trigger, uses paths-ignore: or ! negation, or is gated on another branch.

Verification

Explicitly not fixed here

`.cofoundy/` holds an agent's kickoff prompt, task specs and run state. It was
untracked but NOT ignored, so a single `git add -A` would publish machine paths
and internal tooling detail into a public history — where a push is not undone,
it is archived.

Closing this by infrastructure rather than by the committer's attention.
…#5)

`stop.sh` asserted "this doc is unpublished" from a measurement of "this doc
was edited". Those are different propositions, and two flows sat entirely
inside the gap:

  #4 — repo-backed vault: the mandated flow is commit + push → the repo's
       Action publishes. Nothing local ever cleared the list, so the nag was
       unconditional.
  #5 — the agent runs `basalt publish <file>` mid-turn, which is exactly what
       the nudge asks for, and `stop.sh` was the only writer that cleared the
       list. The nudge could not be satisfied except by not editing.

Mechanism A — publish-tracking. New `post-publish.sh`, a PostToolUse(Bash)
hook mirroring `post-edit.sh`. The dirty list becomes an append-only journal:
edits append absolute paths, publishes append `-<operand>` tombstones, and
Stop replays it. No `basalt status` call (network in the hot path, and the
same issue documents that oracle as lying on image-bearing docs); success
semantics come from the measured harness fact that PostToolUse fires only on
exit 0. Only the last segment of a compound command, or an all-`&&` chain,
may clear anything; a quote, backslash or newline anywhere clears nothing;
`--dry-run` clears nothing.

`post-edit.sh` changes in exactly one way: the dirty append is now
unconditional. `append_once` dedups against history, and under a journal a
tombstoned path stays in that history forever — so a re-edit's append was
skipped and Stop went silent on a re-dirtied doc. `append_once` stays on the
orphan list, where the dedup is semantically required.

Mechanism B — a `stop.sh` git-state filter for the #4 case. All local (`git`
plumbing + `case` globs, no regex, zero network). A doc is dropped only on
positive local evidence that a push of this file, on this branch, starts a
Basalt publish workflow: the candidate workflow is itself tracked, clean and
pushed; the doc has no uncommitted changes and its last commit is an ancestor
of upstream; the branch is the repo's default branch; the workflow has a
`push:` trigger, every `branches:` list contains the branch, every `paths:`
list matches, and nothing negates. Every failure, gap and ambiguity keeps the
nag. Run-step selection and a workflow that fails at runtime stay invisible —
a declared residual, not a closed one.

Tests: 83 assertions, plus an opt-in mutation pass (`tests/mutate.sh`,
15 mutants, all killed) and `tests/probe-harness.sh`, which re-measures the
exit-0 firing policy the design rests on and which the suite cannot pin.

Also sanitizes five already-public fixtures carrying a machine path, an
internal product path and real session UUIDs. Containment, not prevention —
the history stays public.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant